<?php
namespace app\server;

use think\swoole\Server;
use Swoole\Table;
use Swoole\Channel;
use app\service\User;
use app\service\Room;
use app\service\RedEnvelope;
use think\Controller;
use think\Db;

class WebSocket extends Server
{
    protected $host       = '0.0.0.0';
    protected $port       = 9509;
    protected $serverType = 'socket';
    protected $option     = [ 
        'worker_num' => 2,
        'daemonize'  => false,
        'backlog'    => 128
    ];
    protected $daemonize  = false;
    protected static $platformId     = 0;

    /**
     * 初始化用户、Fd、房间内存表
     *
     */
    public function init()
    {
        //单个table数据量上限10k
        //user表格式[uid=>[fd, roomId]]
        $this->swoole->userTable  = new Table(10240);
        $this->swoole->userTable->column('fd', Table::TYPE_INT, 4);
        $this->swoole->userTable->column('roomId', Table::TYPE_INT, 4);
        $this->swoole->userTable->column('roomType', Table::TYPE_INT, 4);
        $this->swoole->userTable->column('name', Table::TYPE_STRING, 18);
        $this->swoole->userTable->column('uid', Table::TYPE_STRING, 18);
        $this->swoole->userTable->column('gameStatus', Table::TYPE_INT, 3);             // 房间游戏是否开始,0---停止，1---开始
        $this->swoole->userTable->column('gameStartTime', Table::TYPE_INT, 3);          // 房间游戏开启时间
        $this->swoole->userTable->create();
        //fd表格式[fd=>[uid]]
        /*$this->swoole->fdTable    = new Table(10240);
        $this->swoole->fdTable->column('uid', Table::TYPE_INT, 4);
        $this->swoole->fdTable->create();*/
        //room表格式[roomId=>[uid]],N个房间对应N个表
        /*$this->swoole->roomTable1 = new Table(10240);
        $this->swoole->roomTable1->column('fd', Table::TYPE_INT, 4);
        $this->swoole->roomTable1->create();

        $this->swoole->roomTable2 = new Table(10240);
        $this->swoole->roomTable2->column('fd', Table::TYPE_INT, 4);
        $this->swoole->roomTable2->create();*/
        //抢红包队列channel
        $this->swoole->channel = new Channel(10240);
    }

    /**
     * Worker进程启动事件,在第一个worker中启动定时器操作抢红包队列
     * @param Swoole\WebSocket\Server $server
     * @param int $worker_id
     * @return null
     */
    public function onWorkerStart($server, $worker_id) {
        if ($worker_id == 0) {
            //抢红包队列操作，每5ms取50条
            $that = $this;
            swoole_timer_tick(5, function ($timer_id) use($server, $that) {
                for ($i = 0; $i < 50; $i++) {
                    $data = $server->channel->pop();
                    if (!$data) break;

                    $haveRobot  = array_key_exists('robot', $data['data']) ? 1 : 0;
                    $haveRobot ?  '': $fd = $data['data']['fd'];
                    $uid        = $data['data']['uid'];
                    $roomId     = $data['data']['roomId'];
                    $rlist      = $this->getRoomList($roomId);
                    if (isset($fd)) {$roomInfo   = $this->swoole->userTable->get($uid);} else {$roomInfo = $data['data']['roomInfo'];}
                    $result     = RedEnvelope::doSnatch($data);
                    if ($result && $result['status'] === 0) 
                    {
                        $reCode     = $result['data']['reCode'];
                        # 抢红包的数据处理成功
                        if (isset($fd)) $that->sendToFd($fd, $result);
                        $result['type'] = 'snatchInfo';
                        $result['data'] = [
                            'reCode'        =>$reCode,
                            "name"          =>$result['data']['name'],
                            "snatchUser"    =>$result['data']['snatchUser'],
                            "snatchMoney"   =>$result['data']['snatchMoney'],
                            "nextUser"      =>$result['data']['nextUser'],
                            "uid"           =>$result['data']['uid'],
                        ];
                        # 判断是否中奖
                        $snatchMoney    = $result['data']['snatchMoney'];
                        $prizeResult    = RedEnvelope::isPrize($snatchMoney,$roomId,$uid,$reCode);
                        $prizeName      = $result['data']['snatchUser'];               
                        $prizeUid       = $result['data']['uid'];               
                        if ($prizeResult) 
                        {
                            # 如果中奖在该房间广播
                            if ($prizeUid != static::$platformId) 
                            {
                                # 当中奖的不为系统，才广播到所有房间
                                $prizeArr = ['type' => 'prizeMes', 'status' => 0, 'data' => ['name' => $prizeName, 'prize' => $prizeResult, 'snatch' => $snatchMoney]];
                                $that->myBroadcast($prizeArr,$rlist);
                            }
                            
                        }
                      	if (!is_array($roomInfo)) {var_dump($roomInfo.'--uid--'.$uid);var_dump($data['data']);}
                        $roomInfo['roomType'] = array_key_exists('roomType', $roomInfo) ? $roomInfo['roomType'] : $roomInfo['type'];
                        if ($roomInfo['roomType'] == 1) 
                        {
                            # 如果是扫雷房间判断是否踩雷
                            $stepResult = RedEnvelope::isStepThunder($snatchMoney,$roomId,$uid,$reCode);
                            if ($stepResult) 
                            {
                                $that->myBroadcast($stepResult,$rlist); 
                            }
                            # 如果当前红包已抢完统计发出雷的个数,判断是否奖励
                            $sendThunderPrize = RedEnvelope::sendThunderPrize($roomId,$reCode,$uid,$roomInfo['roomType']);
                            if($sendThunderPrize) $that->myBroadcast($sendThunderPrize,$rlist);
                        }
                        if ($result['data']['nextUser'] === 1 && $roomInfo['roomType'] == 2) 
                        {
                            // echo '执行抢操作1--'.$uid;
                            # 红包已抢完找到记录中的最小的发包
                            $result = RedEnvelope::nextPocket($result['data']['reCode'],$roomId);
                            $sendFd = $roomInfo['fd'];
                            
                            $this->sendToFd($sendFd, $result);
                            $result['type'] = 'redEnvInfo';
                            unset($result['data']['moneyInfo']);
                            $that->myBroadcast($result,$rlist);
                            $pdata   = RedEnvelope::snatch(0, $fd, $roomId, $result);                        // 平台首先抢一个
                            if ($pdata['status'] === 0) 
                            {
                                $server->channel->push($pdata);
                            }
                        }
                    }
                    else if ($result && $result['status'] == 2) {
                        # 2代表平台抢红包，不需要广播
                    } 
                    else {
                        var_dump($result);
                        $that->sendToFd($fd, $result);
                    }
                }
            });
        }
    }

    /**
     * 客户端连接事件
     * @param Swoole\WebSocket\Server $server
     * @param Swoole\Http\Request $request
     * @return null
     */
    public function onOpen($server, $request) {
        echo "server: handshake success with fd{$request->fd}\n";
        // die;
        $this->login($request);

    }

    /**
     * 客户端消息事件
     * @param Swoole\WebSocket\Server $server
     * @param Swoole\WebSocket\Frame $frame
     * @return null
     */
    public function onMessage($server, $frame) {
        echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
        //$server->push($frame->fd, "this is server");
        // var_dump($frame);
        $data 	= json_decode($frame->data, true);
        $fd 	= (string) $frame->fd;
        // $user    = $this->getUser($fd);
        $user 	= $this->getRoomByFd($fd);
        $uid    = $user['uid'];
        $roomId = $user['roomId'];
        $rlist  = $this->getRoomList($roomId);
        $type 	= isset($data['type']) ? $data['type'] : '';
        switch ($type) {
            //ping/pong保持客户端在线，不因长时间没有发送数据被路由器断开连接
            case 'ping':
                $this->sendToFd($fd, ['type'=>'pong','status'=>0,'time'=>date('Y-m-d H:i:s')]);
                break;
            // 开始游戏
            case 'start':
                $this->startGame($rlist);                                                       // 修改房间里所有用户的状态
                $result = RedEnvelope::start($uid, $roomId);                                    // 发红包,添加数据库记录
                $this->myBroadcast($result,$rlist);                                             // 广播到该房间所有用户

                $data   = RedEnvelope::snatch(0, $fd, $roomId, $result);                        // 平台首先抢一个
                if ($data['status'] === 0) 
                {
                    $server->channel->push($data);
                }
                break;
            //发红包
            case 'send':
                $result     = RedEnvelope::send($uid, $roomId, $data);
                // echo '<pre>';
                // var_dump($result);
                if ($result['status'] === 0) {
                    $roomInfo   = $result['data']['roomInfo'];
                    $this->sendToFd($fd, $result);
                    $result['type'] = 'redEnvInfo';
                    unset($result['data']['moneyInfo']);
                    $this->myBroadcast($result, $rlist);
                    $res = $result;
                    if($result['data']['roomType'] == 1)
                    {
                        # 如果是扫雷，平台先抢一个
                        $data = RedEnvelope::snatch(0, $fd, $roomId, $res);
                        if ($data['status'] === 0) 
                        {
                            $server->channel->push($data);
                        }
                        if ($roomInfo['robot_switch'] == 1) $this->setTime($roomInfo,$server,$result);
                    }
                } else {
                    $this->sendToFd($fd, $result);
                }
                break;
            //拆红包
            case 'open':
                $data['data']['reCode'] = $data['reCode'];
                $result = RedEnvelope::open($uid, $roomId, $data);
                $this->sendToFd($fd, $result);
                break;
            //抢红包（仅加入队列）
            case 'snatch':
                $result = RedEnvelope::snatch($uid, $fd, $roomId, $data);       // data  --类型、红包id
                if ($result['status'] === 0) {
                    $server->channel->push($result);
                } else {
                    # 返回status!==0直接在客户端弹出错误信息
                    $this->sendToFd($fd, $result);
                }
                break;
            //用户列表
            case 'users':
                $result = $this->myusers($roomId);
                $this->sendToFd($fd, $result);
                break;
            default:
                $this->sendToFd($fd, ['type'=>'notice','status'=>1,'msg'=>'消息类型错误']);
        }
    }

    // 设置定时器
    public function setTime($roomInfo,$server,$result)
    {
        $rlist  = $this->getRoomList($roomInfo['room_id']);
        # 设置定时器到时间判断是否抢包、是否发包
        $timing = $roomInfo['send_frequency'] + mt_rand(1,20);
        var_dump($timing);
        var_dump($result['data']['reCode']);
        swoole_timer_after($timing * 1000, function () use ($roomInfo,$server,$rlist,$result) {
            $res    = $result; 
            $result = RedEnvelope::robotSnatch($result['data']['reCode'],$roomInfo);
            if ($result)
            {
                foreach ($result['uid'] as $value) 
                {
                    $data['data']           = $result;
                    $data['data']['uid']    = $value;
                    var_dump('id--'.$value);
                    $server->channel->push($data);
                }
            }

            $robotResult     = RedEnvelope::robotSend($roomInfo,$res['data']['reCode']);
            if($robotResult) 
            {
                $robotResult['type']    = 'redEnvInfo';
                $this->myBroadcast($robotResult,$rlist);
                # 平台先抢一个
                $fd = 0;
                $roomId = $roomInfo['room_id'];
                $data = RedEnvelope::snatch(0, $fd, $roomId, $robotResult);
                if ($data['status'] === 0) 
                {
                    $server->channel->push($data);
                }
                $this->setTime($roomInfo,$server,$robotResult);
            }
            
        });
    }

    public function onRequest($request, $response) {
        $response->end("<h1>Hello Swoole. #" . rand(1000, 9999) . "</h1>");
    }

    /**
     * 客户端断开连接事件（包括服务端主动断开），对Fd内存表、用户内存表、房间内存表进行清理
     * @param Swoole\WebSocket\Server $server
     * @param string $fd
     * @return null
     */
    public function onClose($server, $fd) {
        echo "client {$fd} closed\n";
        $fd = (string) $fd;
        foreach ($this->swoole->userTable as $key=>$value) 
        {
            # 查找userTable中对应fd的数据
            if ($value['fd'] == $fd) 
            {
                # 清空用户表中的数据
                $this->swoole->userTable->del($key);
                # 根据房间号获取user表中该房间的数据
                $rlist = $this->getRoomList($value['roomId']);          
                $this->myBroadcast(['type' => 'logout', 'status' => 0, 'data' => ['uid' => $value['uid'], 'name' => $value['name']]], $rlist);
            }
        }
        //清理用户表、fd表、房间表
        /*if ($this->swoole->fdTable->exist($fd)) {
            $fdTableItem = $this->swoole->fdTable->get($fd);
            $uid = $fdTableItem['uid'];
            if ($this->swoole->userTable->exist($uid)) {
                $userTableItem = $this->swoole->userTable->get($uid);
                $roomId = $userTableItem['roomId'];
                $roomTable = $this->getRoomTable($roomId);
                if ($this->swoole->$roomTable->exist($uid)) {
                    $this->swoole->$roomTable->del($uid);
                    $this->broadcast(['type' => 'logout', 'status' => 0, 'data' => ['uid' => $uid, 'name' => $userTableItem['name']]], $roomId);
                }
                $this->swoole->userTable->del($uid);
            }
            $this->swoole->fdTable->del($fd);
        }*/
    }

    /**
     * 踢人
     *
     * @param string $fd
     * @param string $reason
     * @return null
     */
    public function kick($fd, $reason = '')
    {
        $data = [
            'type' => 'kick',
            'msg' => $reason,
        ];
        $this->sendToFd($fd, $data);
        $this->swoole->close($fd);
    }

    /**
     * 广播(暂时只有用到登录)
     *
     * @param array $data 发送数据
     * @param int $rlist 房间里所有用户
     * @param int $expUid 排除用户id
     * @return bool
     */
    public function myBroadcast($data, $rlist)
    {
        // var_dump($rlist);
        if (count($rlist)) 
        {
            foreach ($rlist as $item) 
            {
                $this->sendToFd($item['fd'], $data);
            }
        } 
        /*else {
            foreach ($this->swoole->connections as $fd) {
                $this->sendToFd($fd, $data);
            }
        }*/
    }

    /**
     * 广播(后期考虑使用任务投递，避免广播人数太多造成阻塞)
     *
     * @param array $data 发送数据
     * @param int $roomId 房间id,如果为0则表示所有房间
     * @param int $expUid 排除用户id
     * @return bool
     */
    /*public function broadcast($data, $roomId = 0, $expUid = 0)
    {
        if ($roomId) {
            // $roomTable = $this->getRoomTable($roomId);
            foreach ($this->swoole->$roomTable as $item) {
                $this->sendToFd($item['fd'], $data);
            }
        } else {
            foreach ($this->swoole->connections as $fd) {
                $this->sendToFd($fd, $data);
            }
        }
    }*/

    /**
     * 获取房间内存表表名
     *
     * @param string $roomId
     * @return string
     */
    /*public function getRoomTable($roomId)
    {
        return 'roomTable'.$roomId;
    }*/

    /**
     * 获取用户内存表数据
     *
     * @param string $fd
     * @param string $roomType
     * @return array
     */
    public function getUser($fd)
    {
        $default = [
            'fd' => '0',
            'roomId' => '0',
            'name' => '',
        ];
        $user = $this->swoole->userTable->get($fd);
        //var_dump($uid, $user);
        if ($user) return $user;
        return $default;
    }

    /**
     * 根据Fd获取user内存表对应房间
     * @descript 不用 
     * @param string $fd
     * @return string
     */
    public function getRoomByFd($fd)
    {
        $user = $this->swoole->userTable;
        foreach ($user as $key => $value) 
        {
            # 拿出user表中fd等于当前连接号的数据
            if ($value['fd'] == $fd) 
            {
                return $room = $value;
            }
        }
    }

    /**
     * 登录
     *
     * @param Swoole\Http\Request $request
     * @return null
     */
    public function login($request)
    {
        $fd = (string) $request->fd;
        $token      = !empty($request->get['token']) ? $request->get['token'] : '';
        $roomType   = !empty($request->get['roomType']) ? $request->get['roomType'] : '';
        $user       = Db::name('user')->where(['user_token' => $token])->find();
        if (!$token || !$user) 
        {
        	# 检测token
        	$this->kick($fd, '您的账号在其他登录111');
        }
        $uid 		= (string) $user['user_id'];
        //房间id
        $roomId 	= isset($request->get['roomId']) ? $request->get['roomId'] : 0;
        // 获取数据库房间列表
        $roomInfo = Db::name('room')->where(['type' => $roomType, 'room_id' => $roomId])->find();
        // $roomTable 	= $this->getRoomTable($roomId);
        if (!$roomId || !$roomInfo) 
        {
            $this->kick($fd, '房间信息错误');
            return;
        }
        $roomId = (string) $roomId;
        //判断用户是否符合加入房间条件
        //...
        // 获取内存表当前房间中的所有数据
        $nowRoom = $this->getRoomList($roomId);
        $rConnectN = count($nowRoom)+1;
        
        if ($rConnectN >= $roomInfo['pocket']) 
        {
            # 判断房间是否已满
            $this->kick($fd, '该房间已满');
        }
        if ($this->swoole->userTable->exist($uid)) 
        {
            # 判断是否已经在其他房间登录，是则踢掉之前连接
            $userTableItem = $this->swoole->userTable->get($uid);
            $this->kick($userTableItem['fd'], '您的账号在其他登录');
        }

        $uName = $user['user_name'] ? $user['user_name'] : $user['user_phone'];
        if ($rConnectN < 1) 
        {
            # 如果当前用户是该房间第一位用户初始化状态
            $this->swoole->userTable->set($uid, ['fd' => $fd, 'roomId' => $roomId, 'name' => $uName, 'uid' => $uid, 'gameStatus' => 0, 'roomType' => $roomType, 'gameStartTime' =>date("Y-m-d H:i:s")]);
        }
        else
        {
            $gameInfo = $this->getRoomInfo($roomId);
            # 成功加入房间
            $this->swoole->userTable->set($uid, ['fd' => $fd, 'roomId' => $roomId, 'name' => $uName, 'uid' => $uid, 'gameStatus' => $gameInfo[0], 'roomType' => $roomType, 'gameStartTime' =>$gameInfo[1]]);
            if ($gameInfo[0] == 1 && $roomType == 2) 
            {
                # 如果房间中游戏是开启状态，向当前加入者推送最后一个未领完的红包,并禁用页面的上的开始开关
                $result = RedEnvelope::pushUncollected($roomId,$roomType);
                $this->sendToFd($fd, $result);
            }
            if ($roomType == 1) 
            {
                # 扫雷红包推送
                $result = RedEnvelope::pushUncollected($roomId,$roomType);
                if($result['status'] ===0){$this->sendToFd($fd, $result);}
            }
            /*else
            {
                # 
                $this->sendToFd($fd,['type' => 'pushUncollected', 'status' =>1, 'msg' => '数据操作失败'];);
            }*/
        }
        
        // $this->swoole->userTable->set($uid, ['fd' => $fd, 'roomId' => $roomId, 'name' => $uName]);
        // $this->swoole->fdTable->set($fd, ['uid' => $uid]);
        // $this->swoole->$roomTable->set($uid, ['fd' => $fd]);
        // var_dump('房间'.$roomId.'人数：'.$rNum.'房间类型为：'.$roomType);
        // $uTable = count($this->swoole->userTable);
        //欢迎消息        
        $user = $this->getUser($uid);
        unset($user['fd']);
        // $user['uid'] = $uid;
        $data = [
            'type'      => 'login',
            'status'    => 0,
            'msg'       => '',
            'data'      => $user
        ];
        // 根据房间类型，判断是否需要连接数
        $rNum = count($rlist = $this->getRoomList($roomId));
        $gameStatus = $this->swoole->userTable->get($uid)['gameStatus'];
        if ($roomType == 2) 
        {
            $rNum = $rNum ? $rNum : 0;
            # 房间类型为2(为接龙)，返回连接数
            $data['data']['num'] = $rNum+1;
            $data['data']['sum'] = $roomInfo['pocket'];
            $data['data']['gameStatus'] = $gameStatus;
        }

        // $roomId = $this->getUser($uid)['roomId'];
        $this->myBroadcast($data, $rlist);
    }

    /**
     * 发送数据到指定客户端
     *
     * @param string $fd
     * @param array $data
     * @return null
     */
    public function sendToFd($fd, $data)
    {
        if ($this->swoole->exist($fd))
        {
            $this->swoole->push($fd, json_encode($data));
        }
    }

    /**
     * 获取用户列表指定房间信息
     * description 
     * @param string $roomId
     * @return array
     */
    public function getRoomList($roomId)
    {
        $allUTable = $this->swoole->userTable;
        $onlyRoom = [];
        foreach ($allUTable as $key => $value) 
        {
            # 调出该房间的信息，组装成数组
            if ($value['roomId'] == $roomId)
            {
                array_push($onlyRoom, $value);
            }
        }
        if (!$onlyRoom) 
        {
            return [];
        }
        return $onlyRoom;
    }

    /**
     * 获取用户列表指定房间信息
     * description 用不用待确定
     * @param string $roomId
     * @return array
     */
    public function users($roomId)
    {
        $data = [];
        $roomTable = $this->getRoomTable($roomId);
        foreach ($this->swoole->$roomTable as $uid => $fd) {
            //$this->sendToFd($fd, $data);
            $user = $this->getUser($uid);
            $data[] = ['uid' => $uid, 'name' => $user['name']];
        }
        return ['type' => 'usersResult', 'status' => 0, 'msg' => '', 'data' => $data];
    }

    /**
     * 获取用户列表指定房间信息
     * description 
     * @param string $roomId
     * @return array
     */
    public function myusers($roomId)
    {
        $data = [];
        $data = $this->getRoomList($roomId);
        /*foreach ($roomTable as $uid => $u) 
        {
            //$this->sendToFd($fd, $data);
            $user   = $this->getUser($uid);
            $data[] = ['uid' => $uid, 'name' => $user['name']];
        }*/  
        return ['type' => 'usersResult', 'status' => 0, 'msg' => '', 'data' => $data];
    }

    /**
     * 房间开启游戏
     * description
     * @param array $rlist
     */
    public function startGame($rlist)
    {
        foreach ($rlist as $value) 
        {
            # 批量修改游戏状态
            $this->swoole->userTable->set($value['uid'], ['fd' => $value['fd'], 'roomId' => $value['roomId'], 'name' => $value['name'], 'uid' => $value['uid'], 'gameStatus' => 1, 'gameStartTime' => date("Y-m-d H:i:s")]);
        }
    }

    /**
     * 获取当前房间的游戏信息
     * description
     * @param string $roomId
     * @return array $gameInfo
     */
    public function getRoomInfo($roomId)
    {
        $userTable = $this->swoole->userTable;
        foreach ($userTable as $key => $value) 
        {
            # 查找一条该房间的用户数据，获得游戏状态
            if ($value['roomId'] == $roomId) 
            {
                return [$value['gameStatus'],$value['gameStartTime']]; 
            }
        }
    }
}
